package com.google.code.jtasks.bugzilla.proxy.bug;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.client.XmlRpcClient;

import com.google.code.jtasks.bugzilla.proxy.ProcessingUtils;

/**
 * This class provides a proxy for the <code>Bug.fields</code> XML-RPC method.
 */
public final class BugFieldsProxy
{
	private final XmlRpcClient rpcClient;

	/**
	 * Constructs a new {@link BugFieldsProxy} instance.
	 * 
	 * @param rpcClient
	 *            the {@link XmlRpcClient} that should be used
	 */
	public BugFieldsProxy(XmlRpcClient rpcClient)
	{
		this.rpcClient = rpcClient;
	}

	/**
	 * This method is a proxy for the <code>Bug.fields</code> XML-RPC method.
	 * 
	 * @param params
	 *            the {@link FieldsParameters} to call the method with
	 * @return a {@link FieldsResult} object containing the results of the search
	 */
	public FieldsResult fields(FieldsParameters params)
	{
		Object rpcResultStruct;
		try
		{
			rpcResultStruct = rpcClient.execute("Bug.fields", new Object[] { params.convertToRpcStruct() });
		}
		catch (XmlRpcException e)
		{
			// TODO throw a better exception
			throw new RuntimeException(e);
		}

		return new FieldsResult(rpcResultStruct);
	}

	/**
	 * Represents the parameters that can be passed to the <code>Bug.fields</code> XML-RPC method.
	 */
	public static final class FieldsParameters
	{
		/*
		 * Note: The actual method does have parameters, we just don't need them (yet) so I'm ignoring them
		 * for now.
		 */

		/**
		 * Constructs a new {@link FieldsParameters} instance.
		 */
		public FieldsParameters()
		{
		}

		/**
		 * Converts this {@link FieldsParameters} instance to a "raw" struct that can be passed to an
		 * {@link XmlRpcClient}.
		 * 
		 * @return a "raw" struct that can be passed to an {@link XmlRpcClient}
		 */
		Map<String, Object> convertToRpcStruct()
		{
			Map<String, Object> rpcStruct = new HashMap<String, Object>();
			return rpcStruct;
		}
	}

	/**
	 * Represents the result returned by the <code>Bug.fields</code> XML-RPC method.
	 */
	public static final class FieldsResult
	{
		private final List<Field> fields;

		/**
		 * Constructs a new {@link FieldsResult} instance from the specified
		 * {@link XmlRpcClient#execute(String, Object[])} result object.
		 * 
		 * @param rpcResultStruct
		 *            the {@link XmlRpcClient#execute(String, Object[])} result object to build the
		 *            {@link FieldsResult} from
		 */
		public FieldsResult(Object rpcResultStruct)
		{
			Map<?, ?> rootMap = ProcessingUtils.castToMap(rpcResultStruct);
			Object[] fieldsArray = ProcessingUtils.extractObjectArrayField(rootMap, "fields", false);

			// Walk through each field in the array, extracting it to a typed object
			this.fields = new ArrayList<Field>();
			for (Object fieldStruct : fieldsArray)
			{
				Field fieldModel = new Field(fieldStruct);
				this.fields.add(fieldModel);
			}
		}

		/**
		 * Returns the {@link Field}s that were found in the XML-RPC result object.
		 * 
		 * @return the {@link Field}s that were found in the XML-RPC result object
		 */
		public List<Field> getFields()
		{
			return fields;
		}

		/**
		 * Returns the {@link Field} in {@link #getFields()} with the specified {@link Field#getName()}.
		 * 
		 * @param fieldName
		 *            the {@link Field#getName()} to match
		 * @param failIfMissing
		 *            whether an {@link IllegalArgumentException} should be thrown if a matching {@link Field}
		 *            cannot be found, rather than just returning <code>null</code>
		 * @return the {@link Field} in {@link #getFields()} with the specified {@link Field#getName()}
		 * @throws IllegalArgumentException
		 *             An {@link IllegalArgumentException} will be thrown if a matching {@link Field} is not
		 *             found and <code>failIfMissing</code> is <code>true</code>.
		 */
		public Field getField(String fieldName, boolean failIfMissing)
		{
			if (fieldName == null)
				throw new IllegalAccessError("Invalid field name.");

			for (Field field : fields)
				if (fieldName.equals(field.getName()))
					return field;

			if (failIfMissing)
				throw new IllegalArgumentException("Unable to find priority field in specified FieldsResult.");
			else
				return null;
		}
	}

	/**
	 * Represents a Bugzilla field, as returned by the <code>Bug.fields</code> XML-RPC method.
	 */
	public static final class Field implements Serializable
	{
		private final Integer id;
		private final Integer type;
		private final Boolean is_custom;
		private final String name;
		private final String display_name;
		private final Boolean is_mandatory;
		private final Boolean is_on_bug_entry;
		private final String visibility_field;
		private final String[] visibility_values;
		private final String value_field;
		private final List<FieldValue> values;

		/**
		 * Constructs a new {@link Field} instance from the specified
		 * {@link XmlRpcClient#execute(String, Object[])} result inner object.
		 * 
		 * @param fieldStruct
		 *            the {@link XmlRpcClient#execute(String, Object[])} result inner object to build the
		 *            {@link Field} from
		 */
		public Field(Object fieldStruct)
		{
			Map<?, ?> fieldMap = ProcessingUtils.castToMap(fieldStruct);

			this.id = ProcessingUtils.extractIntegerField(fieldMap, "id", false);
			this.type = ProcessingUtils.extractIntegerField(fieldMap, "type", false);
			this.is_custom = ProcessingUtils.extractBooleanField(fieldMap, "is_custom", false);
			this.name = ProcessingUtils.extractStringField(fieldMap, "name", false);
			this.display_name = ProcessingUtils.extractStringField(fieldMap, "display_name", false);
			this.is_mandatory = ProcessingUtils.extractBooleanField(fieldMap, "is_mandatory", false);
			this.is_on_bug_entry = ProcessingUtils.extractBooleanField(fieldMap, "is_on_bug_entry", false);
			this.visibility_field = ProcessingUtils.extractStringField(fieldMap, "visibility_field", true);
			this.visibility_values = ProcessingUtils.extractStringArrayField(fieldMap, "visibility_values");
			this.value_field = ProcessingUtils.extractStringField(fieldMap, "value_field", true);

			// Walk through each value in the values array, extracting it to a typed object
			Object[] valuesArray = ProcessingUtils.extractObjectArrayField(fieldMap, "values", true);
			if (valuesArray != null)
			{
				this.values = new ArrayList<FieldValue>();
				for (Object valueStruct : valuesArray)
				{
					FieldValue valueModel = new FieldValue(valueStruct);
					this.values.add(valueModel);
				}
			}
			else
			{
				this.values = null;
			}
		}

		/**
		 * Returns the value of this field, as returned by the RPC call.
		 * 
		 * @return the value of this field, as returned by the RPC call
		 * @see http://www.bugzilla.org/docs/4.0/en/html/api/Bugzilla/WebService/Bug.html#name
		 */
		public String getName()
		{
			return name;
		}

		/**
		 * Returns the {@link FieldValue}s that represent the <code>"values"</code> of this {@link Field} in
		 * the RPC result.
		 * 
		 * @return the {@link FieldValue}s that represent the <code>"values"</code> of this {@link Field} in
		 *         the RPC result
		 * @see http://www.bugzilla.org/docs/4.0/en/html/api/Bugzilla/WebService/Bug.html#values
		 */
		public List<FieldValue> getValues()
		{
			return Collections.unmodifiableList(values);
		}
	}

	/**
	 * Represents an allowed Bugzilla field value, as returned by the <code>Bug.fields</code> XML-RPC method.
	 */
	public static final class FieldValue implements Serializable
	{
		private static final long serialVersionUID = 7316299156634427867L;

		private final String name;
		private final Integer sortkey;
		private final String[] visibility_values;
		private final Boolean is_open;
		private final List<FieldValueTransition> valueTransitions;

		/**
		 * Constructs a new {@link FieldValue} instance from the specified
		 * {@link XmlRpcClient#execute(String, Object[])} result inner object.
		 * 
		 * @param fieldValueStruct
		 *            the {@link XmlRpcClient#execute(String, Object[])} result inner object to build the
		 *            {@link FieldValue} from
		 */
		public FieldValue(Object fieldValueStruct)
		{
			Map<?, ?> fieldMap = ProcessingUtils.castToMap(fieldValueStruct);

			this.name = ProcessingUtils.extractStringField(fieldMap, "name", true);
			this.sortkey = ProcessingUtils.extractIntegerField(fieldMap, "sortkey", true);
			this.visibility_values = ProcessingUtils.extractStringArrayField(fieldMap, "visibility_values");
			this.is_open = ProcessingUtils.extractBooleanField(fieldMap, "is_open", true);

			// Walk through each value in the can_change_to array, extracting it to a typed object
			Object[] canChangeToArray = ProcessingUtils.extractObjectArrayField(fieldMap, "can_change_to",
					true);
			if (canChangeToArray != null)
			{
				this.valueTransitions = new ArrayList<FieldValueTransition>();
				for (Object valueTransitionStruct : canChangeToArray)
				{
					FieldValueTransition valueTransitionModel = new FieldValueTransition(
							valueTransitionStruct);
					this.valueTransitions.add(valueTransitionModel);
				}
			}
			else
			{
				this.valueTransitions = null;
			}
		}

		/**
		 * Returns the value of this field, as returned by the RPC call.
		 * 
		 * @return the value of this field, as returned by the RPC call
		 * @see http://www.bugzilla.org/docs/4.0/en/html/api/Bugzilla/WebService/Bug.html#name
		 */
		public String getName()
		{
			return name;
		}

		/**
		 * Returns the value of this field, as returned by the RPC call.
		 * 
		 * @return the value of this field, as returned by the RPC call
		 * @see http://www.bugzilla.org/docs/4.0/en/html/api/Bugzilla/WebService/Bug.html#sortkey
		 */
		public Integer getSortkey()
		{
			return sortkey;
		}
	}

	/**
	 * Represents an allowed Bugzilla field value transition, as returned by the <code>Bug.fields</code>
	 * XML-RPC method.
	 */
	public static final class FieldValueTransition implements Serializable
	{
		private static final long serialVersionUID = -2134425026957700211L;

		private final String name;
		private final Boolean comment_required;

		/**
		 * Constructs a new {@link FieldValueTransition} instance from the specified
		 * {@link XmlRpcClient#execute(String, Object[])} result inner object.
		 * 
		 * @param fieldValueTransitionStruct
		 *            the {@link XmlRpcClient#execute(String, Object[])} result inner object to build the
		 *            {@link FieldValueTransition} from
		 */
		public FieldValueTransition(Object fieldValueTransitionStruct)
		{
			Map<?, ?> fieldTransitionMap = ProcessingUtils.castToMap(fieldValueTransitionStruct);

			this.name = ProcessingUtils.extractStringField(fieldTransitionMap, "name", true);
			this.comment_required = ProcessingUtils.extractBooleanField(fieldTransitionMap,
					"comment_required", false);
		}
	}
}
